package close.mazegame.level;

import java.util.ArrayList;

import box2dLight.ConeLight;
import box2dLight.Light;
import box2dLight.PointLight;
import box2dLight.RayHandler;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.World;

import close.mazegame.MazeGame;
import close.mazegame.art.tileset.FloorTileSet;
import close.mazegame.art.tileset.WallTileSet;
import close.mazegame.collision.CollisionListener;
import close.mazegame.entity.Entity;
import close.mazegame.entity.PhysicsEntity;
import close.mazegame.entity.PhysicsEntity.EntityCategory;
import close.mazegame.entity.effect.Effect;
import close.mazegame.entity.mob.Fleer;
import close.mazegame.entity.mob.AITank;
import close.mazegame.entity.player.Player;
import close.mazegame.entity.player.PlayerAttributes;
import close.mazegame.entity.projectile.Projectile;
import close.mazegame.pathfinding.Pathfinder;
import close.mazegame.screens.GameScreen;
import close.mazegame.tile.Tile;
import close.mazegame.tile.VoidTile;
import close.util.AngleMath;

public abstract class Level {
	
	protected static final float DELTA_LIGHTS_ON = 0.1f; // Time inbetween lights turning on on completion
	protected static final float END_LIGHT_BRIGHTNESS = 0.7f;

	protected final int width;
	protected final int height;
	
	private OrthographicCamera cam;

	protected WallTileSet wallTileSet;
	protected FloorTileSet floorTileSet;

	protected Tile[][] tiles;
	protected ArrayList<VoidTile> boundaryTiles;
	protected Vector2 start;
	protected Vector2 end;

	protected World world;
	protected CollisionListener listener;
	protected Pathfinder pathfinder;
	protected RayHandler rayHandler;

	protected Player player;

	protected EntityManager<Fleer> fleers;
	protected EntityManager<PhysicsEntity> backgroundEntities;
	protected EntityManager<AITank> mobs;
	protected EntityManager<Projectile> projectiles;
	protected EntityManager<Effect> effects;
	protected EntityManager<Entity> miscEntities;

	protected LevelState currentState;

	// Lighting up on completion
	protected float tLightsOn = 0;           // Time since last light has been turned on
	protected int nLight = 0;                // Depth to which lights have been turned on

	public enum LevelState {
		INPLAY, COMPLETE, GAMEOVER
	}

	public Level(OrthographicCamera cam, int w, int h) {
		this(cam, w, h, new PlayerAttributes());
	}

	public Level(OrthographicCamera cam, int w, int h, PlayerAttributes attributes) {
		this.width = w;
		this.height = h;
		
		this.cam = cam;

		this.world = new World(new Vector2(0, 0), true);

		this.listener = new CollisionListener();
		this.world.setContactListener(listener);
		
		RayHandler.useDiffuseLight(true);
		this.rayHandler = new RayHandler(world);
		Light.setContactFilter((short)0x0001, (short)0, (short)EntityCategory.SOLIDTILE.getBits());

		this.pathfinder = new Pathfinder(this);

		this.fleers = new EntityManager<Fleer>();
		this.backgroundEntities = new EntityManager<PhysicsEntity>();
		this.mobs = new EntityManager<AITank>();
		this.projectiles = new EntityManager<Projectile>();
		this.effects = new EntityManager<Effect>();
		this.miscEntities = new EntityManager<Entity>();
		
		//Set the default tilesets
		this.wallTileSet = WallTileSet.primary;
		this.floorTileSet = FloorTileSet.primary;

		this.tiles = new Tile[width][height];
		this.boundaryTiles = new ArrayList<VoidTile>();
		this.currentState = LevelState.INPLAY;

		createLevel();
		addEntites();
		
		addPlayer(attributes);
	}

	protected abstract void createLevel();

	protected abstract void addEntites();

	protected abstract void addPlayer(PlayerAttributes attributes);

	public void update(float delta, OrthographicCamera cam) {
		if (fleers.getSize() == 0 && player.isAtEnd()) {
			currentState = LevelState.COMPLETE;
		} else if (player.getAttributes().getCurrentHealth() <= 0) {
			currentState = LevelState.GAMEOVER;
		}

		player.update(delta);

		pathfinder.setTarget(player.getX(), player.getY());
		pathfinder.update();

		backgroundEntities.update(delta);
		fleers.update(delta);
		mobs.update(delta);
		projectiles.update(delta);
		effects.update(delta);
		miscEntities.update(delta);

		world.step(delta, 6, 2);

		rayHandler.setCombinedMatrix(cam.combined);
		rayHandler.update();
	}

	public void updateLevelComplete(float delta, OrthographicCamera cam) {
		rayHandler.setCombinedMatrix(cam.combined);
		rayHandler.update();
		lightsOn(delta);
	}

	protected void lightsOn(float delta) {
		tLightsOn += delta;
		if (tLightsOn > DELTA_LIGHTS_ON) {
			tLightsOn -= DELTA_LIGHTS_ON;
			nLight += 1;
			for (int x = 0; x < width; x++) {
				for (int y = 0; y < height; y++) {
					if (!tiles[x][y].isSolid()) {
						if (pathfinder.getDistance(x, y) == nLight) {
							addPointLight(x + Tile.SIZE / 2.0f, y + Tile.SIZE / 2.0f, 16, new Color(1.0f, 1.0f, 1.0f, END_LIGHT_BRIGHTNESS), 2.0f, 1.0f);
						}
					}
				}
			}
		}
	}

	public void render(GameScreen screen) {
		for (Tile[] tiles1D : tiles) {
			for (Tile t : tiles1D) {
				t.render(screen);
			}
		}

		backgroundEntities.render(screen);
		fleers.render(screen);
		mobs.render(screen);
		player.render(screen);
		projectiles.render(screen);
		effects.render(screen);
	}

	public void renderUI(GameScreen screen) {
		if (fleers.getSize() == 0) {
			screen.drawString("Get to the exit!", 0, MazeGame.HEIGHT - 24, 3);
		} else {
			screen.drawString("Targets remaining: " + Integer.toString(fleers.getSize()), 0, MazeGame.HEIGHT - 24, 3);
		}
		int playerHealth = player.getAttributes().getCurrentHealth();
		screen.drawString("Health: " + Integer.toString(playerHealth), MazeGame.WIDTH - 300, MazeGame.HEIGHT - 24, 3);
	}

	//There is probably a better way of doing this
	public void add(Entity e) {
		if (e instanceof AITank) {
			if (e instanceof Fleer) {
				fleers.add((Fleer) e);
			} else {
				mobs.add((AITank) e);
			}
		} else if (e instanceof Projectile) {
			projectiles.add((Projectile) e);
		} else if (e instanceof PhysicsEntity) {
			backgroundEntities.add((PhysicsEntity)e);
		} else if (e instanceof Effect) {
			effects.add((Effect)e);
		} else {
			miscEntities.add(e);
		}
	}

	public void removeBody(Body b) {
		world.destroyBody(b);
	}

	public Vector2 getPlayerPosition() {
		return player.getPosition();
	}

	public Tile[][] getTiles() {
		return tiles;
	}

	public boolean isSolid(int x, int y) {
		if (x < 0 || x >= width || y < 0 || y >= height) {
			return true;
		}
		return tiles[x][y].isSolid();
	}

	public int getWidth() {
		return width;
	}

	public int getHeight() {
		return height;
	}

	public Pathfinder getPathfinder() {
		return pathfinder;
	}

	public World getWorld() {
		return world;
	}

	public void renderLights() {
		rayHandler.render();
	}

	public boolean lineOfSightToPlayer(Vector2 pos, float angle, float fov) {
		float playerAngle = player.getPosition().sub(pos).angle();
		float da = Math.abs(AngleMath.angleDifference(angle, playerAngle));
		if (da > fov / 2.0) {
			return false;
		} else {
			return lineOfSightToPlayer(pos);
		}
	}

	public boolean lineOfSightToPlayer(Vector2 pos) {
		return lineOfSight(pos.x, pos.y, player.getX(), player.getY());
	}

	public boolean lineOfSight(float x0, float y0, float x1, float y1) {
		float dx = Math.abs(x1 - x0);
		float dy = Math.abs(y1 - y0);

		int x = (int) Math.floor(x0);
		int y = (int) Math.floor(y0);

		int n = 1;
		int x_inc = 0;
		int y_inc = 0;
		double error = 0;

		if (dx == 0) {
			x_inc = 0;
			error = 1e10; // This must be larger than any error likely to be encountered from y
		} else if (x1 > x0) {
			x_inc = 1;
			n += (int) Math.floor(x1) - x;
			error = (Math.floor(x0) + 1 - x0) * dy;
		} else {
			x_inc = -1;
			n += x - (int) Math.floor(x1);
			error = (x0 - Math.floor(x0)) * dy;
		}

		if (dy == 0) {
			y_inc = 0;
			error -= 1e10;
		} else if (y1 > y0) {
			y_inc = 1;
			n += (int) Math.floor(y1) - y;
			error -= (Math.floor(y0) + 1 - y0) * dx;
		} else {
			y_inc = -1;
			n += y - (int) Math.floor(y1);
			error -= (y0 - Math.floor(y0)) * dx;
		}

		for (int i = n; i > 0; i--) {
			if (tiles[x][y].isSolid()) {
				return false;
			}

			if (error > 0) {
				y += y_inc;
				error -= dx;
			} else {
				x += x_inc;
				error += dy;
			}
		}

		return true;
	}

	public PointLight addPointLight(float x, float y, int rays, Color color, float distance, float softness) {
		PointLight pl = new PointLight(rayHandler, rays, color, distance, x, y);
		pl.setSoftnessLength(softness);
		return pl;
	}

	public PointLight addPointLight(Body body, int rays, Color color, float distance, float softness) {
		PointLight pl = new PointLight(rayHandler, rays, color, distance, body.getPosition().x, body.getPosition().y);
		pl.setSoftnessLength(softness);
		pl.attachToBody(body, 0, 0);
		return pl;
	}

	public ConeLight addConeLight(Body body, int rays, Color color, float distance, float softness, int spread) {
		ConeLight cl = new ConeLight(rayHandler, rays, color, distance, body.getPosition().x, body.getPosition().y, body.getAngle(), spread);
		cl.setSoftnessLength(softness);
		cl.attachToBody(body, 0, 0);
		return cl;
	}

	public LevelState getState() {
		return currentState;
	}

	public PlayerAttributes getAttributes() {
		return player.getAttributes();
	}

	public void dispose() {
		world.dispose();
	}
	
	public OrthographicCamera getCam() {
		return cam;
	}

}
